package org.apache.lucene.search;

/**
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

import java.io.IOException;
import java.util.ArrayList;
import java.util.List;

import org.apache.lucene.search.BooleanClause.Occur;

/* See the description in BooleanScorer.java, comparing
 * BooleanScorer & BooleanScorer2 */

/** An alternative to BooleanScorer that also allows a minimum number
 * of optional scorers that should match.
 * <br>Implements skipTo(), and has no limitations on the numbers of added scorers.
 * <br>Uses ConjunctionScorer, DisjunctionScorer, ReqOptScorer and ReqExclScorer.
 */
class BooleanScorer2 extends Scorer {

    private final List<Scorer> requiredScorers;
    private final List<Scorer> optionalScorers;
    private final List<Scorer> prohibitedScorers;

    private class Coordinator {
        float[] coordFactors = null;
        int maxCoord = 0; // to be increased for each non prohibited scorer
        int nrMatchers; // to be increased by score() of match counting scorers.

        void init(Similarity sim, boolean disableCoord) { // use after all scorers have been added.
            coordFactors = new float[optionalScorers.size() + requiredScorers.size() + 1];
            for (int i = 0; i < coordFactors.length; i++) {
                coordFactors[i] = disableCoord ? 1.0f : sim.coord(i, maxCoord);
            }
        }
    }

    private final Coordinator coordinator;

    /** The scorer to which all scoring will be delegated,
     * except for computing and using the coordination factor.
     */
    private final Scorer countingSumScorer;

    /** The number of optionalScorers that need to match (if there are any) */
    private final int minNrShouldMatch;

    private int doc = -1;

    /**
     * Creates a {@link Scorer} with the given similarity and lists of required,
     * prohibited and optional scorers. In no required scorers are added, at least
     * one of the optional scorers will have to match during the search.
     * 
     * @param weight
     *          The BooleanWeight to be used.
     * @param disableCoord
     *          If this parameter is true, coordination level matching 
     *          ({@link Similarity#coord(int, int)}) is not used.
     * @param minNrShouldMatch
     *          The minimum number of optional added scorers that should match
     *          during the search. In case no required scorers are added, at least
     *          one of the optional scorers will have to match during the search.
     * @param required
     *          the list of required scorers.
     * @param prohibited
     *          the list of prohibited scorers.
     * @param optional
     *          the list of optional scorers.
     */
    public BooleanScorer2(Weight weight, boolean disableCoord, Similarity similarity, int minNrShouldMatch, List<Scorer> required, List<Scorer> prohibited, List<Scorer> optional, int maxCoord) throws IOException {
        super(weight);
        if (minNrShouldMatch < 0) {
            throw new IllegalArgumentException("Minimum number of optional scorers should not be negative");
        }
        coordinator = new Coordinator();
        this.minNrShouldMatch = minNrShouldMatch;
        coordinator.maxCoord = maxCoord;

        optionalScorers = optional;
        requiredScorers = required;
        prohibitedScorers = prohibited;

        coordinator.init(similarity, disableCoord);
        countingSumScorer = makeCountingSumScorer(disableCoord, similarity);
    }

    /** Count a scorer as a single match. */
    private class SingleMatchScorer extends Scorer {
        private Scorer scorer;
        private int lastScoredDoc = -1;
        // Save the score of lastScoredDoc, so that we don't compute it more than
        // once in score().
        private float lastDocScore = Float.NaN;

        SingleMatchScorer(Scorer scorer) {
            super(scorer.weight);
            this.scorer = scorer;
        }

        @Override
        public float score() throws IOException {
            int doc = docID();
            if (doc >= lastScoredDoc) {
                if (doc > lastScoredDoc) {
                    lastDocScore = scorer.score();
                    lastScoredDoc = doc;
                }
                coordinator.nrMatchers++;
            }
            return lastDocScore;
        }

        @Override
        public int docID() {
            return scorer.docID();
        }

        @Override
        public int nextDoc() throws IOException {
            return scorer.nextDoc();
        }

        @Override
        public int advance(int target) throws IOException {
            return scorer.advance(target);
        }
    }

    private Scorer countingDisjunctionSumScorer(final List<Scorer> scorers, int minNrShouldMatch) throws IOException {
        // each scorer from the list counted as a single matcher
        return new DisjunctionSumScorer(weight, scorers, minNrShouldMatch) {
            private int lastScoredDoc = -1;
            // Save the score of lastScoredDoc, so that we don't compute it more than
            // once in score().
            private float lastDocScore = Float.NaN;

            @Override
            public float score() throws IOException {
                int doc = docID();
                if (doc >= lastScoredDoc) {
                    if (doc > lastScoredDoc) {
                        lastDocScore = super.score();
                        lastScoredDoc = doc;
                    }
                    coordinator.nrMatchers += super.nrMatchers;
                }
                return lastDocScore;
            }
        };
    }

    private Scorer countingConjunctionSumScorer(boolean disableCoord, Similarity similarity, List<Scorer> requiredScorers) throws IOException {
        // each scorer from the list counted as a single matcher
        final int requiredNrMatchers = requiredScorers.size();
        return new ConjunctionScorer(weight, disableCoord ? 1.0f : similarity.coord(requiredScorers.size(), requiredScorers.size()), requiredScorers) {
            private int lastScoredDoc = -1;
            // Save the score of lastScoredDoc, so that we don't compute it more than
            // once in score().
            private float lastDocScore = Float.NaN;

            @Override
            public float score() throws IOException {
                int doc = docID();
                if (doc >= lastScoredDoc) {
                    if (doc > lastScoredDoc) {
                        lastDocScore = super.score();
                        lastScoredDoc = doc;
                    }
                    coordinator.nrMatchers += requiredNrMatchers;
                }
                // All scorers match, so defaultSimilarity super.score() always has 1 as
                // the coordination factor.
                // Therefore the sum of the scores of the requiredScorers
                // is used as score.
                return lastDocScore;
            }
        };
    }

    private Scorer dualConjunctionSumScorer(boolean disableCoord, Similarity similarity, Scorer req1, Scorer req2) throws IOException { // non counting.
        return new ConjunctionScorer(weight, disableCoord ? 1.0f : similarity.coord(2, 2), new Scorer[] { req1, req2 });
        // All scorers match, so defaultSimilarity always has 1 as
        // the coordination factor.
        // Therefore the sum of the scores of two scorers
        // is used as score.
    }

    /** Returns the scorer to be used for match counting and score summing.
     * Uses requiredScorers, optionalScorers and prohibitedScorers.
     */
    private Scorer makeCountingSumScorer(boolean disableCoord, Similarity similarity) throws IOException { // each scorer counted as a single matcher
        return (requiredScorers.size() == 0) ? makeCountingSumScorerNoReq(disableCoord, similarity) : makeCountingSumScorerSomeReq(disableCoord, similarity);
    }

    private Scorer makeCountingSumScorerNoReq(boolean disableCoord, Similarity similarity) throws IOException { // No required scorers
        // minNrShouldMatch optional scorers are required, but at least 1
        int nrOptRequired = (minNrShouldMatch < 1) ? 1 : minNrShouldMatch;
        Scorer requiredCountingSumScorer;
        if (optionalScorers.size() > nrOptRequired)
            requiredCountingSumScorer = countingDisjunctionSumScorer(optionalScorers, nrOptRequired);
        else if (optionalScorers.size() == 1)
            requiredCountingSumScorer = new SingleMatchScorer(optionalScorers.get(0));
        else {
            requiredCountingSumScorer = countingConjunctionSumScorer(disableCoord, similarity, optionalScorers);
        }
        return addProhibitedScorers(requiredCountingSumScorer);
    }

    private Scorer makeCountingSumScorerSomeReq(boolean disableCoord, Similarity similarity) throws IOException { // At least one required scorer.
        if (optionalScorers.size() == minNrShouldMatch) { // all optional scorers also required.
            ArrayList<Scorer> allReq = new ArrayList<Scorer>(requiredScorers);
            allReq.addAll(optionalScorers);
            return addProhibitedScorers(countingConjunctionSumScorer(disableCoord, similarity, allReq));
        } else { // optionalScorers.size() > minNrShouldMatch, and at least one required scorer
            Scorer requiredCountingSumScorer = requiredScorers.size() == 1 ? new SingleMatchScorer(requiredScorers.get(0)) : countingConjunctionSumScorer(disableCoord, similarity, requiredScorers);
            if (minNrShouldMatch > 0) { // use a required disjunction scorer over the optional scorers
                return addProhibitedScorers(dualConjunctionSumScorer( // non counting
                                disableCoord, similarity, requiredCountingSumScorer, countingDisjunctionSumScorer(optionalScorers, minNrShouldMatch)));
            } else { // minNrShouldMatch == 0
                return new ReqOptSumScorer(addProhibitedScorers(requiredCountingSumScorer), optionalScorers.size() == 1 ? new SingleMatchScorer(optionalScorers.get(0))
                                // require 1 in combined, optional scorer.
                                : countingDisjunctionSumScorer(optionalScorers, 1));
            }
        }
    }

    /** Returns the scorer to be used for match counting and score summing.
     * Uses the given required scorer and the prohibitedScorers.
     * @param requiredCountingSumScorer A required scorer already built.
     */
    private Scorer addProhibitedScorers(Scorer requiredCountingSumScorer) throws IOException {
        return (prohibitedScorers.size() == 0) ? requiredCountingSumScorer // no prohibited
                        : new ReqExclScorer(requiredCountingSumScorer, ((prohibitedScorers.size() == 1) ? prohibitedScorers.get(0) : new DisjunctionSumScorer(weight, prohibitedScorers)));
    }

    /** Scores and collects all matching documents.
     * @param collector The collector to which all matching documents are passed through.
     */
    @Override
    public void score(Collector collector) throws IOException {
        collector.setScorer(this);
        while ((doc = countingSumScorer.nextDoc()) != NO_MORE_DOCS) {
            collector.collect(doc);
        }
    }

    @Override
    protected boolean score(Collector collector, int max, int firstDocID) throws IOException {
        doc = firstDocID;
        collector.setScorer(this);
        while (doc < max) {
            collector.collect(doc);
            doc = countingSumScorer.nextDoc();
        }
        return doc != NO_MORE_DOCS;
    }

    @Override
    public int docID() {
        return doc;
    }

    @Override
    public int nextDoc() throws IOException {
        return doc = countingSumScorer.nextDoc();
    }

    @Override
    public float score() throws IOException {
        coordinator.nrMatchers = 0;
        float sum = countingSumScorer.score();
        return sum * coordinator.coordFactors[coordinator.nrMatchers];
    }

    @Override
    public float freq() {
        return coordinator.nrMatchers;
    }

    @Override
    public int advance(int target) throws IOException {
        return doc = countingSumScorer.advance(target);
    }

    @Override
    protected void visitSubScorers(Query parent, Occur relationship, ScorerVisitor<Query, Query, Scorer> visitor) {
        super.visitSubScorers(parent, relationship, visitor);
        final Query q = weight.getQuery();
        for (Scorer s : optionalScorers) {
            s.visitSubScorers(q, Occur.SHOULD, visitor);
        }
        for (Scorer s : prohibitedScorers) {
            s.visitSubScorers(q, Occur.MUST_NOT, visitor);
        }
        for (Scorer s : requiredScorers) {
            s.visitSubScorers(q, Occur.MUST, visitor);
        }
    }
}
